--[[
CastYeller2 - Announce important spell events.

Copyright (C) 2009 Adirelle

This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
--]]

--------------------------------------------------------------------------------
-- Addon declaration
--------------------------------------------------------------------------------

CastYeller2 = LibStub("AceAddon-3.0"):NewAddon("CastYeller2", "AceEvent-3.0", "AceConsole-3.0")
local CastYeller2 = CastYeller2

local L = CastYeller2_L
CastYeller2.L = L
CastYeller2_L = nil

local announceDefaultsDB = {
	enabled = true,
	inCombat = false,
	names = { ['*'] = false },
	sources = { ['*'] = false },
	triggers = { ['*'] = false },
	sinks = { ['*'] = true },
	custom = {
		['*'] = {
			enabled = false,
			announces = {},
		}
	},
}

CastYeller2.defaultAnnouncesDB = {
	['**'] = announceDefaultsDB,
}

local defaultTemplates = { ['*'] = {} }

local defaultsDB = {
	profile = {
		enabled = true,
		squareBrackets = false,
		conditionals = {
			['*'] = false,
		},
		modules = {
			['*'] = true,
		},
		triggers = {
			['*'] = true,
		},
		templates = defaultTemplates,
		announces = CastYeller2.defaultAnnouncesDB,
	},
}
CastYeller2.defaultsDB = defaultsDB

--------------------------------------------------------------------------------
-- Internal messaging thingy
--------------------------------------------------------------------------------

do
	local fakelib = {}
	local callbacks = LibStub('CallbackHandler-1.0'):New(fakelib, "RegisterSignal", "UnregisterSignal", "UnregisterAllSignals")
	CastYeller2.RegisterSignal = fakelib.RegisterSignal
	CastYeller2.UnregisterSignal = fakelib.UnregisterSignal
	CastYeller2.UnregisterAllSignals = fakelib.UnregisterAllSignals
	CastYeller2.SendSignal = callbacks.Fire
	function callbacks:OnUsed(target, event)
		self:Fire('OnSignalUsed', event)
	end
	function callbacks:OnUnused(target, event)
		self:Fire('OnSignalUnused', event)
	end
end

--------------------------------------------------------------------------------
-- Table recycling
--------------------------------------------------------------------------------

local new, del, newHash
do
	local tremove, select, setmetatable, wipe = table.remove, select, setmetatable, wipe
	local heap = setmetatable({}, {__mode='kv'})
	function new(...)
		local t = tremove(heap)
		if t then
			for i = 1, select('#', ...) do
				t[i] = select(i, ...)
			end
		else
			t = { ... }
		end
		return t
	end
	function newHash(...)
		local t = new()
		for i = 1, select('#', ...), 2 do
			local k, v = select(i, ...)
			t[k] = v
		end
		return t
	end
	function del(t)
		if type(t) == "table" then
			setmetatable(t, nil)
			wipe(t)
			heap[t] = true
		end
	end
end
CastYeller2.new, CastYeller2.newHash, CastYeller2.del = new, newHash, del

--------------------------------------------------------------------------------
-- State handling
--------------------------------------------------------------------------------

function CastYeller2:OnInitialize()
	self:MergeDefaultsDB()

	self.db = LibStub('AceDB-3.0'):New("CastYeller2DB", defaultsDB)
	self.db.RegisterCallback(self, 'OnProfileChanged', 'Reboot')
	self.db.RegisterCallback(self, 'OnProfileCopied', 'Reboot')
	self.db.RegisterCallback(self, 'OnProfileReset', 'Reboot')
	self:SetEnabledState(self.db.enabled)

	self:RegisterChatCommand("cy2", "OpenConfigGUI", true)
	self:RegisterChatCommand("castyeller2", "OpenConfigGUI", true)

	self:RegisterSignal('OnConfigChanged')
	self:RegisterSignal('OnConfigLoaded')
	self:RegisterSignal('OnAnnounceCreated', "OnConfigLoaded")
	self:RegisterSignal('OnAnnounceDeleted', "OnConfigLoaded")

	LibStub('LibDataBroker-1.1'):NewDataObject('CastYeller2', {
		type = 'launcher',
		icon = 'Interface\\Icons\\Ability_Warrior_BattleShout',
		OnClick = function(frame, button) self:OpenConfigGUI() end,
	})
end

local firstStart = true
function CastYeller2:OnEnable()
	if firstStart then
		firstStart = false
		self:SendSignal('OnConfigLoaded')
	end
	self:RegisterEvent('PLAYER_ENTERING_WORLD', "UpdateModuleState")
	self:RegisterEvent('PLAYER_UPDATE_RESTING', "UpdateModuleState")
	self:RegisterEvent('PARTY_MEMBERS_CHANGED', "UpdateModuleState")	
	self:RegisterEvent('ZONE_CHANGED',          "UpdateModuleState")
	self:RegisterEvent('ZONE_CHANGED_INDOORS',  "UpdateModuleState")
	self:RegisterEvent('ZONE_CHANGED_NEW_AREA', "UpdateModuleState")
end

function CastYeller2:Reboot()
	self:Disable()
	self:SendSignal('OnConfigLoaded')
	self:Enable()
end

function CastYeller2:OpenConfigGUI()
	LoadAddOn('CastYeller2_Config')
end

function CastYeller2:OnConfigLoaded()
	self:UpdateActiveAnnounces()
	self:UpdateWatchedNames()
	self:UpdateModuleState()
end

do
	local stateKeys = { modules = true, enabled = true, triggers = true, sinks = true, sources = true, conditionals = true }
	function CastYeller2:OnConfigChanged(_, info)
		local key = info.arg or info[#info]
		if type(key) == "table" then
			key = key[#key]
		end
		-- Only refresh active announces, name and modules on certain changes
		if key and stateKeys[key] then
			self:UpdateActiveAnnounces()
			self:UpdateWatchedNames()
			self:UpdateModuleState()
		elseif key == 'names' then
			self:UpdateWatchedNames()
		end
	end
end

do
	local activeAnnounces = {}

	-- Build a list of announces that have both enabled triggers and enabled sinks
	function CastYeller2:UpdateActiveAnnounces()	
		wipe(activeAnnounces)
		for name, announce in self:IterateAnnounces(true) do
			local hasActiveTrigger, hasActiveSink
			for trigger, source in self:IterateTriggers(true) do
				if announce.sources[source] and announce.triggers[trigger] then
					hasActiveTrigger = true
					break
				end
			end
			for sink in self:IterateSinkModules(true) do
				if announce.sinks[sink] then
					hasActiveSink = true
					break
				end
			end
			if hasActiveTrigger and hasActiveSink then
				activeAnnounces[name] = announce
			end
		end
	end
	
	function CastYeller2:IterateActiveAnnounces()
		return pairs(activeAnnounces)
	end
end

do
	local watchedNames = {}

	-- Build a list of active names by source names
	function CastYeller2:UpdateWatchedNames()
		for moduleName in self:IterateSourceModules() do
			if not watchedNames[moduleName] then
				watchedNames[moduleName] = {}
			else
				wipe(watchedNames[moduleName])
			end
		end
		for _, announce in self:IterateActiveAnnounces() do
			for moduleName, module in self:IterateSourceModules(true) do
				if announce.sources[moduleName] then
					for trigger in pairs(module.triggers) do
						if announce.triggers[trigger] then
							local t = watchedNames[moduleName]
							for name in pairs(announce.names) do
								t[name] = true
							end
							break
						end
					end
				end
			end
		end
	end

	function CastYeller2:IsNameWatched(name, source)
		return source and name and watchedNames[source] and watchedNames[source][name]
	end

	function CastYeller2:IterateWatchedNames(source)
		return pairs(watchedNames[source])
	end
	
	function CastYeller2:IsWatchingNames(source)
		return watchedNames[source] and next(watchedNames[source]) and true or false
	end	

end

local function SetModuleStates(states)
	local enabled = CastYeller2:IsEnabled()
	for name, module in CastYeller2:IterateModules() do
		if states and states[name] then
			if enabled then
				module:Enable()
			else
				module:SetEnabledState(true)
			end
		else
			module:Disable()
		end
	end
end

local GetActiveModules
do
	local modules = {}
	function GetActiveModules()
		wipe(modules)
		for source in CastYeller2:IterateSourceModules(true) do
			if CastYeller2:IsWatchingNames(source) then
				modules[source] = true				
			end
		end
		for sink in CastYeller2:IterateSinkModules(true) do
			for name, announce in CastYeller2:IterateActiveAnnounces() do
				if announce.sinks[sink] then
					modules[sink] = true
					break
				end
			end
		end
		return modules
	end
end

function CastYeller2:UpdateModuleState()
	local modules
	local conditionals = self.db.profile.conditionals
	local instanceType = (GetMapInfo() == 'LakeWintergrasp') and 'pvp' or select(2, IsInInstance())
	if not (
			(conditionals.resting and IsResting()) or
			(conditionals.soloing and GetNumPartyMembers() == 0) or
			(conditionals.arena and instanceType == 'arena') or
			(conditionals.battleground and instanceType == 'pvp') or
			(conditionals.groupInstance and instanceType == 'party') or
			(conditionals.raidInstance and instanceType == 'raid') or
			(conditionals.outsideInstances and instanceType == 'none')
		) then
		modules = GetActiveModules()
	end
	SetModuleStates(modules)
end

--------------------------------------------------------------------------------
-- Helper iterators
--------------------------------------------------------------------------------

function CastYeller2:IterateAnnounces(onlyEnabled)
	return function(announces, name)
		local announce
		repeat
			name, announce = next(announces, name)
			if name and announce and (not onlyEnabled or announce.enabled) then
				return name, announce
			end
		until not name
	end, self.db.profile.announces
end

local function IterateModules(self, onlyEnabled, moduleType)
	local moduleIterator, moduleIteratorData = CastYeller2:IterateModules()
	if onlyEnabled then
		local enabledModules = self.db.profile.modules
		return function(_, name)
			local module
			repeat
				name, module = moduleIterator(moduleIteratorData, name)
				if name and module.moduleType == moduleType and enabledModules[name] then
					return name, module
				end
			until not name
		end
	else
		return function(_, name)
			local module
			repeat
				name, module = moduleIterator(moduleIteratorData, name)
				if name and module.moduleType == moduleType then
					return name, module
				end
			until not name
		end
	end
end

function CastYeller2:IterateSourceModules(onlyEnabled)
	return IterateModules(self, onlyEnabled, 'source')
end

function CastYeller2:IterateSinkModules(onlyEnabled)
	return IterateModules(self, onlyEnabled, 'sink')
end

function CastYeller2:IterateTriggers(onlyEnabled)
	local sourceIterator, sourceIteratorData = self:IterateSourceModules(onlyEnabled)
	local	sourceName, sourceModule = sourceIterator(sourceIteratorData)
	local enabledTriggers = self.db.profile.triggers
	return function(_, trigger)
		repeat
			if sourceName then
				repeat
					trigger = next(sourceModule.triggers, trigger)
					if trigger and (not onlyEnabled or enabledTriggers[trigger]) then
						return trigger, sourceName
					end
				until not trigger
			end
			sourceName, sourceModule = sourceIterator(sourceIteratorData, sourceName)
		until not sourceName
	end
end

--------------------------------------------------------------------------------
-- New module handling
--------------------------------------------------------------------------------

function CastYeller2:RegisterModule(module)
	self:SendSignal('OnModuleRegistered', module.moduleName, module)
	if type(module.OnRegister) == "function" then
		module:OnRegister()
	end
	self:UpdateModuleState()
end

local function mergeDefaults(dst, src)
	for k,v in pairs(src) do
		if type(v) == 'table' and type(dst[k]) == 'table' then
			mergeDefaults(dst[k], v)
		else
			dst[k] = v
		end
	end
end

function CastYeller2:MergeDefaultsDB()
	for name, module in self:IterateModules() do
		if module.announceDefaultsDB then
			mergeDefaults(announceDefaultsDB, module.announceDefaultsDB)
			module.announceDefaultsDB = nil
		end
		defaultTemplates[name] = module.defaultAnnounces
	end
end

function CastYeller2:NewSourceModule(name, ...)
	return self:NewModule(name, self.sourceModulePrototype, ...), self.sourceModulePrototype
end

function CastYeller2:NewSinkModule(name, ...)
	return self:NewModule(name, self.sinkModulePrototype, ...), self.sinkModulePrototype
end


